/**
 *  @file HttpEndPoint.cpp
 */
#include "HttpEndPoint.h"
#include "../../text/CFString.h"
#include "../Socket.h"
#include "../ssl/SSLSocket.h"
#include "../../security/cert/X509Certificate.h"
#include "../../collections/KeyValueListT.h"
#include "../../collections/BinBuf.h"
#include "../../convert/Convert.h"
#include "../../io/Stream.h"
#include "message/IHttpBody.h"
#include "HttpException.h"
#include "../../util/zip/Deflater.h"

using namespace cppflib::net::http::message;
using namespace cppflib::collections;
using namespace cppflib::exceptions;
using namespace cppflib::io;
using namespace cppflib::util::zip;

namespace cppflib
{

namespace net
{

namespace http
{


PRIVATE const sz_t READBUFSIZE = 256;
PRIVATE const sz_t SENDLENGTH = 4096;


//////////////////////// static functions /////////////////////////////

/**
 *  Send bytes available in buffer
 */
PRIVATE bool __SendRawBytes(Socket &sock, BinBuf &buffer) 
{
   int byteLen = static_cast<int>(buffer.GetValidLength());
   u8_t * pBuf = buffer.GetRawBuf();
   bool isSendOK = true;
   do {
      int ret = sock.Send(pBuf, byteLen, 0);
      if (ret <= 0) {
         isSendOK = false;
         break;
      }

      byteLen -= ret;
      pBuf += ret;

      if (byteLen <= 0)
         break;
   } while(true);

   return isSendOK;
}

/**
 *  Convert string to bytes and send it to peer
 */
bool HttpEndPoint::SendString(Socket &sock, CFString &srcStr) 
{
   int byteLen = 0;
   srcStr.ToUtf8String(NULL, 0, &byteLen);
   pTempBuf->ClearBuf();
   pTempBuf->Realloc(byteLen + 1);  // to hold '\0' at the end
   srcStr.ToUtf8String(reinterpret_cast<char*>(pTempBuf->GetRawBuf()), 
                       static_cast<int>(pTempBuf->GetLength()), &byteLen);
   pTempBuf->SetValidLength(byteLen);

   return __SendRawBytes(sock, *pTempBuf);
}

/**
 *  @return pointer just beyond the first "\r\n"
 */
PRIVATE u8_t * __GetLineEnd(u8_t *pStart, u8_t *pEnd)
{
   u8_t * pLineEnd = NULL;
   while(pStart < pEnd) {
      if (*pStart == '\r') {
         ++pStart;
         if (pStart >= pEnd)
            break;

         if (*pStart == '\n') {
            pLineEnd = pStart + 1;
            break;
         }
         else {
            --pStart;
         }
      }

      ++pStart;
   }

   return pLineEnd;
}

/**
 *  Retrieve HTTP version from request/status line
 */
bool HttpEndPoint::__GetHttpVersion(const CFString &line, CFString &ver, int &endOfVer)
{
   const pcwstr_t HTTP_LABEL = _S("HTTP/");
   int len = CFString::WStrlen(HTTP_LABEL);
   if (!line.StartsWith(HTTP_LABEL))
      return false;

   int pos = line.Find(_S(' '));  // find SP
   if (pos < 0)
      pos = line.GetLength();

   ver = line.Mid(len, pos - len);
   endOfVer = pos;

   return true;
}

/////////////////////// end of static functions ///////////////////////////////////////

const sz_t HttpEndPoint::STREAMBUFFERSIZE = 1024;


HttpEndPoint::HttpEndPoint(void)
{
   this->pParams = NULL;
   this->pTempBuf = NULL;
}

HttpEndPoint::HttpEndPoint(HttpParams * pParams)
{
   this->pParams = pParams;
   cppmempool::Retain(pParams);

   this->pTempBuf = new BinBuf(READBUFSIZE);
   cppmempool::Retain(pTempBuf);


   // initialize based on HttpParams

   if (pParams->Exists(HttpParams::P_ENDPOINTSOCKET)) {
      AssignSocket(static_cast<Socket*>(const_cast<BaseObject*>(pParams->GetParam(HttpParams::P_ENDPOINTSOCKET))));
   }
   
}

HttpEndPoint::~HttpEndPoint(void)
{
   ReleaseBeforeThrowing();
}


void HttpEndPoint::ReleaseBeforeThrowing()
{
   if (pParams) {
      cppmempool::Release(pParams);
      pParams = NULL;
   }

   if (pTempBuf) {
      cppmempool::Release(pTempBuf);
      pTempBuf = NULL;
   }

   RemoveSocket(false);
}

bool HttpEndPoint::IsServerSide()
{
   return (pParams->Exists(HttpParams::P_ISSERVER)) ?
                  pParams->GetBooleanParam(HttpParams::P_ISSERVER) : false;
}

bool HttpEndPoint::IsSocketValid()
{
   return (pSocket != NULL);
}

bool HttpEndPoint::IsSSLSocket()
{
   if (pSocket != NULL)
      return pSocket->IsAttributeSet(Socket::SSL_SENDRECV_WITHSSL);

   return false;
}

/**
 *  @param [in] isClose -- true to close it explicitly
 */
void HttpEndPoint::RemoveSocket(bool isClose)
{
   if (isClose && pSocket != NULL) {
      pSocket->Close();
   }

   if (pSocket) {
      cppmempool::Release(pSocket);
      pSocket = NULL;
   }
}

void HttpEndPoint::AssignSocket(Socket *pNewSocket)
{
   cppmempool::Retain(pNewSocket);
   RemoveSocket(false);
   this->pSocket = pNewSocket;
}   

/**
 *  Get the first socket (HTTP client only use the 1st slot)
 */
Socket & HttpEndPoint::GetSock()
{
   return *this->pSocket;
}

/**
 *  Fetch one line (end with "\r\n") from buffer received
 *  
 *  @param [out] line -- output line without "\r\n" at the end
 *
 *  @return true a line is fetched
 */
bool HttpEndPoint::GetOneLine(CFString &line)
{
   line = _S("");

   u8_t * pBuf = pTempBuf->GetRawBuf();
   u8_t * pLineEnd = __GetLineEnd(pBuf, pTempBuf->GetValidEnd());

   if (!pLineEnd) 
      return false;

   *(pLineEnd - 2) = '\0';
   line = reinterpret_cast<const char*>(pBuf);
   pTempBuf->MoveData2Front(static_cast<sz_t>(pLineEnd - pBuf)); // remove this line

   return true;
}

/**
 *  Send headers
 */
void HttpEndPoint::SendHeaders(HttpMessage &httpMsg)
{
   CFString headers;
   for (int i = 0; i < static_cast<int>(httpMsg.GetHeaderCount()); ++i) {
      CFString *name, *value;
      httpMsg.GetHeader(i, name, value);
      headers.AppendFormat(_S("%s: %s\r\n"), static_cast<pcwstr_t>(*name), static_cast<pcwstr_t>(*value));
   }
   headers.AppendFormat(_S("\r\n"));

   if (!SendString(GetSock(), headers))
      throw HttpException(_S("Send HTTP headers error"));
}


/**
 *  Send body normally
 */
void HttpEndPoint::SendBodyNormally(HttpMessage &httpMsg)
{
   ArrayByte contBuf(READBUFSIZE);
   IHttpBody *pBody = httpMsg.GetHttpBody();
   
   u64_t bodyLength = pBody->GetContentLength();
   u64_t bytesSent = 0;
   pTempBuf->ClearBuf();

   while (bytesSent < bodyLength) {
      int bytesRead = pBody->Read(contBuf);
      if (bytesRead <= 0)
         break;

      bytesSent += bytesRead;
      pTempBuf->AppendData(contBuf.GetRawPtr(), bytesRead);
      // accumulate more before send 
      if (pTempBuf->GetValidLength() >= SENDLENGTH) { 
         if (!__SendRawBytes(GetSock(), *pTempBuf))
            throw HttpException(_S("Send HTTP body error"));
         pTempBuf->SetValidLength(0);
      }
   }  // end of while

   // send residue
   if (pTempBuf->GetValidLength() > 0) {
      if (!__SendRawBytes(GetSock(), *pTempBuf))
         throw HttpException(_S("Send HTTP body error"));
   }

   if (bytesSent != bodyLength)
      throw HttpException(_S("Num of bytes sent not equal to body length"));
   
}

PRIVATE const char * CHUNKSIZELINE_PLACEHOLDER = "ffff;      \r\n";

/**
 *  Reserve some space on the "chunk size" line
 */
PRIVATE void __ReserveChunkSizeLine(BinBuf &buffer)
{
   buffer.CopyData(reinterpret_cast<const u8_t*>(CHUNKSIZELINE_PLACEHOLDER), 
                   CFString::Strlen(CHUNKSIZELINE_PLACEHOLDER));
}

/**
 *  Finalize a chunk by filling the chunk size and append a "\r\n" at the end
 */
PRIVATE void __FinalizeChunk(BinBuf &buffer, sz_t chunkSize)
{
   sz_t orgLen = buffer.GetValidLength();
   buffer.SetValidLength(0);
   buffer.AppendHex(chunkSize);

   // fill up remaining spaces
   buffer.AppendData(reinterpret_cast<const u8_t*>(";"), 1);
   CFString::Memset(buffer.GetValidEnd(), 'a', 
                    CFString::Strlen(CHUNKSIZELINE_PLACEHOLDER) - 2 - buffer.GetValidLength());
   
   // restore buffer length and append "\r\n"
   buffer.SetValidLength(orgLen);
   buffer.AppendData(reinterpret_cast<const u8_t*>("\r\n"), 2);
}

/**
 *  Send body in chunks
 */
void HttpEndPoint::SendBodyInChunk(HttpMessage &httpMsg)
{
   sz_t curChunkSize = 0;
   ArrayByte contBuf(READBUFSIZE);
   IHttpBody *pBody = httpMsg.GetHttpBody();
      
   pTempBuf->ClearBuf();
   __ReserveChunkSizeLine(*pTempBuf);

   while (true) {
      int bytesRead = pBody->Read(contBuf);
      if (bytesRead <= 0)
         break;

      curChunkSize += bytesRead;
      pTempBuf->AppendData(contBuf.GetRawPtr(), bytesRead);
      // accumulate more before send 
      if (curChunkSize >= SENDLENGTH) { 
         __FinalizeChunk(*pTempBuf, curChunkSize);
         if (!__SendRawBytes(GetSock(), *pTempBuf))
            throw HttpException(_S("Send chunked HTTP body error"));

         curChunkSize = 0;
         __ReserveChunkSizeLine(*pTempBuf);
      }
   }  // end of while

   if (curChunkSize > 0) {
      __FinalizeChunk(*pTempBuf, curChunkSize);
      if (!__SendRawBytes(GetSock(), *pTempBuf))
         throw HttpException(_S("Send chunked HTTP body error"));
   }

   // send last chunk
   __ReserveChunkSizeLine(*pTempBuf);
   __FinalizeChunk(*pTempBuf, 0);
   if (!__SendRawBytes(GetSock(), *pTempBuf))
      throw HttpException(_S("Send chunked HTTP body error"));

}

/**
 *  Send body
 */
void HttpEndPoint::SendBody(HttpMessage &httpMsg)
{
   if (httpMsg.HasHttpBody()) {
      if (httpMsg.GetHttpBody()->IsChunked())
         SendBodyInChunk(httpMsg);
      else 
         SendBodyNormally(httpMsg);
   }
}


/**
 *  Check if a complete HTTP header section is received
 * 
 *  @param [out] endOfHdrPos -- contains end of header position within buffer if it is found
 */
PRIVATE bool __AlreadyGetCompleteHeaders(BinBuf &buffer, ptr_t &endOfHdrPos)
{
   u8_t *pBuf = buffer.GetRawBuf();
   u8_t *pBufEnd = buffer.GetValidEnd();

   // find the pattern "\r\n\r\n"

   while (pBuf < pBufEnd) {
      if (*pBuf == '\r') {

         if (pBuf + 4 > pBufEnd)
            break;

         if (pBuf[1] == '\n' && pBuf[2] == '\r' && pBuf[3] == '\n') {
            endOfHdrPos = reinterpret_cast<ptr_t>(pBuf) + 4 - reinterpret_cast<ptr_t>(buffer.GetRawBuf());
            return true;
         }
      }

      ++pBuf;
   }

   return false;
}

/**
 *  Get request/response line and headers
 *
 *  @note Headers fetched is saved in pTempBuf
 */
void HttpEndPoint::GetHeaders(Stream &readStream)
{
   pTempBuf->ClearBuf();
   ArrayByte readBuf(READBUFSIZE);

   ptr_t endOfHdrPos = 0;
   while (true) {
      if (__AlreadyGetCompleteHeaders(*pTempBuf, endOfHdrPos)) {
         //
         // complete header found, just chop the temp buffer up to end of header and 
         // rewind the stream to start of body
         // 
         i64_t rewindSteps = static_cast<i64_t>(pTempBuf->GetValidLength() - endOfHdrPos);
         pTempBuf->SetValidLength(endOfHdrPos);
         readStream.Seek(-rewindSteps, Stream::S_CUR);
         break;
      }

      int nrBytes = readStream.Read(readBuf);
      pTempBuf->AppendData(readBuf.GetRawPtr(), nrBytes);
      if (pTempBuf->GetValidLength() > HEADER_SIZELIMIT)
         throw HttpException(_S("HTTP headers too big in size"), http::HttpHeaderTooLarge);
   }
   
}

/**
 *  Parse headers
 */
void HttpEndPoint::ParseHeaders(HttpMessage &httpMsg)
{
   CFString headerLine;
   CFString curHeaderName;  // current header name
   CFString curHeaderValue; // header value;

   while (true) {
      if (!GetOneLine(headerLine))
         break;
      
      if (headerLine.IsEmpty())  // header end
         break;

      if (headerLine.StartsWith(_S(' ')) || headerLine.StartsWith(_S('\t'))) {
         // header spanning multiple lines
         if (!curHeaderName.IsEmpty())
            httpMsg.SetHeader(curHeaderName, headerLine, false);
      }
      else {
         int pos = headerLine.Find(_S(':'));
         if (pos < 0)
            throw HttpException(_S("Header field name without colon"), http::HeaderFieldNoColon);

         curHeaderName = headerLine.Left(pos);
         curHeaderValue = headerLine.Mid(pos + 1);
         httpMsg.SetHeader(curHeaderName, curHeaderValue);
      }

   } // end of while

}

/**
 *  Compress body 
 */
PRIVATE IHttpBody * __ConvertToCompressedBody(IHttpBody *pOrgBody)
{
   // allocate a buffer with estimated size
   BinBuf *pCompressedBuf = new BinBuf(static_cast<sz_t>(pOrgBody->GetContentLength() / 2)); 
   Deflater deflater(Deflater::GZIP);
   ArrayByte buffer(256);

   // deflate data
   deflater.SetInputStream(pOrgBody);
   int nrBytes = 0;
   do {
      nrBytes = deflater.Deflate(buffer);
      if (nrBytes <= 0)
         break;

      pCompressedBuf->AppendData(buffer.GetRawPtr(), nrBytes);
   } while(true);

   // wrap compressed data with a ByteStreamBody
   IHttpBody *pNewBody = new ByteStreamBody(pCompressedBuf);
   pNewBody->SetChunked(pOrgBody->IsChunked());
   pNewBody->SetContentType(pOrgBody->GetContentType());
   return pNewBody;
}

/**
 *  Init default headers not set by users
 */
void HttpEndPoint::InitDefaultHeaders(HttpMessage &httpMsg)
{
   if (httpMsg.HasHttpBody()) {
      IHttpBody *pBody = httpMsg.GetHttpBody();
      if (pBody->IsCompressed()) {
         // user wants to send in compressed stream
         pBody = __ConvertToCompressedBody(pBody);
         httpMsg.SetHttpBody(pBody);
         httpMsg.SetHeader(_S("Content-Encoding"), _S("gzip"));
      }

      CFString &contentType = pBody->GetContentType();
      if (!contentType.IsEmpty())
         httpMsg.SetHeader(_S("Content-Type"), contentType);

      // either send in chunk or has a content-length
      if (pBody->IsChunked()) 
         httpMsg.SetHeader(_S("Transfer-Encoding"), _S("chunked"));
      else 
         httpMsg.SetHeader(_S("Content-Length"), convert::ToString(pBody->GetContentLength()));
   }

}

/**
 *  Get the cancel context to stop (jumping out of waiting state) this end point at any time
 *
 *  @return a weak reference to the cancel context because the socket may be changed by 
 *          this end point class (HttpClient)
 */
WeakReferenceT<ICancelContext> *HttpEndPoint::GetCancelContext()
{
   if (!pSocket)
      return NULL;
    
   pSocket->SetAttribute(Socket::SA_CANCELWAIT, lang::Boolean(true));
   return new WeakReferenceT<ICancelContext>(pSocket->GetCancelContext());
}

/**
 *  Get peer certificate after GetRequest/IssueRequest (for SSLSocket only)
 *
 *  @return NULL if not successful
 */
security::cert::X509Certificate *HttpEndPoint::GetPeerCertificate()
{
   if (!IsSocketValid())
      return NULL;

   if (!pSocket->IsAttributeSet(Socket::SSL_SENDRECV_WITHSSL)) // not a SSLSocket
      return NULL;

   return static_cast<ssl::SSLSocket*>(pSocket)->GetPeerCertificate();
}

} // end of namespace http

} // end of namespace net

} // end of namespace cppflib
